// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
// Benchmark for UTF-8 decoding

import 'dart:convert';
import 'dart:typed_data';

import 'package:benchmark_harness/benchmark_harness.dart';

import 'datext_latin1_10k.dart';
import 'entext_ascii_10k.dart';
import 'netext_3_10k.dart';
import 'rutext_2_10k.dart';
import 'sktext_10k.dart';
import 'zhtext_10k.dart';

class Utf8Decode extends BenchmarkBase {
  final String language;
  final String text;
  final int size;
  final bool allowMalformed;
  final Utf8Decoder decoder;
  Uint8List data;
  List<int> chunks;
  int totalInputSize;
  int totalOutputSize;

  static String _makeName(String language, int size, bool allowMalformed) {
    String name = "Utf8Decode.$language.";
    name += size >= 1000000
        ? "${size ~/ 1000000}M"
        : size >= 1000 ? "${size ~/ 1000}k" : "$size";
    if (allowMalformed) name += ".malformed";
    return name;
  }

  Utf8Decode(this.language, this.text, this.size, this.allowMalformed)
      : decoder = Utf8Decoder(allowMalformed: allowMalformed),
        super(_makeName(language, size, allowMalformed));

  @override
  void setup() {
    data = utf8.encode(text) as Uint8List;
    if (data.length != 10000) {
      throw "Expected input data of exactly 10000 bytes.";
    }
    if (size < data.length) {
      // Split into chunks.
      chunks = <int>[];
      chunks.add(0);
      for (int pos = size; pos < data.length; pos += size) {
        int chunkPos = pos;
        while ((data[chunkPos] & 0xc0) == 0x80) {
          chunkPos--;
        }
        chunks.add(chunkPos);
      }
      chunks.add(data.length);
      totalInputSize = data.length;
      totalOutputSize = text.length;
    } else if (size > data.length) {
      // Repeat data to the desired length.
      final Uint8List expanded = Uint8List(size);
      for (int i = 0; i < size; i++) {
        expanded[i] = data[i % data.length];
      }
      chunks = <int>[0, size];
      totalInputSize = size;
      totalOutputSize = text.length * size ~/ data.length;
      data = expanded;
    } else {
      // Use data as is.
      chunks = <int>[0, data.length];
      totalInputSize = data.length;
      totalOutputSize = text.length;
    }
  }

  @override
  void run() {
    int lengthSum = 0;
    final int numChunks = chunks.length - 1;
    for (int i = 0; i < numChunks; i++) {
      final String s = decoder.convert(data, chunks[i], chunks[i + 1]);
      lengthSum += s.length;
    }
    if (lengthSum != totalOutputSize) {
      throw "Output length doesn't match expected.";
    }
  }

  @override
  void exercise() {
    // Only a single run per measurement.
    run();
  }

  @override
  void warmup() {
    BenchmarkBase.measureFor(run, 1000);
  }

  @override
  double measure() {
    // Report time per input byte.
    return super.measure() / totalInputSize;
  }

  @override
  void report() {
    // Report time in nanoseconds.
    final double score = measure() * 1000.0;
    print("$name(RunTime): $score ns.");
  }
}

void main(List<String> args) {
  const texts = {
    "en": en,
    "da": da,
    "sk": sk,
    "ru": ru,
    "ne": ne,
    "zh": zh,
  };
  final bool testMalformed =
      args != null && args.isNotEmpty && args.first == "malformed";
  final benchmarks = [
    // Only benchmark with allowMalformed: false unless specified otherwise.
    for (bool allowMalformed in [false, if (testMalformed) true])
      for (int size in [10, 10000, 10000000])
        for (String language in texts.keys)
          () => Utf8Decode(language, texts[language], size, allowMalformed)
  ];

  benchmarks.forEach((bm) => bm().report());
}
